package org.softee.functional;

import static org.softee.functional.functions.Predicates.not;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;

import org.softee.functional.functions.BinaryOperator;
import org.softee.functional.functions.Block;
import org.softee.functional.functions.Mapper;
import org.softee.functional.functions.Predicate;
import org.softee.functional.helper.DelegatingIterables;
import org.softee.functional.helper.Objects;

public class SequentialFunctionalIterable<T> extends AbstractFunctionalIterable<T> implements LazyFunctionalIterable<T> {

    protected SequentialFunctionalIterable(Iterable<T> iterable) {
        super(iterable);
    }

    public static <T> LazyFunctionalIterable<T> from(Iterable<T> iterable) {
        return new SequentialFunctionalIterable<T>(iterable);
    }

    public static <T> LazyFunctionalIterable<T> from(T... elements) {
        return new SequentialFunctionalIterable<T>(Arrays.asList(elements));
    }

    @Override
    public ParallelFunctionalIterable<T> parallel() {
        return AtomizedParallelFunctionalIterable.from(iterable);
    }

    @Override
    public long count() {
        if (iterable instanceof Collection) {
            return ((Collection<T>)iterable).size();
        }
        long size = 0;
        Iterator<T> iterator = iterator();
        while (iterator.hasNext()) {
            iterator.next();
            size++;
        }
        return size;
    }

    @Override
    public boolean allMatch(Predicate<? super T> filter) {
        FunctionalIterable<T> noMatches = filter(not(filter));
        // first time an element does NOT match, we bail out
        return !noMatches.iterator().hasNext();
    }

    @Override
    public boolean anyMatch(Predicate<? super T> filter) {
        return filter(filter).iterator().hasNext();
    }

    @Override
    public boolean noneMatch(Predicate<? super T> filter) {
        return !filter(filter).iterator().hasNext();
    }

    @Override
    public LazyFunctionalIterable<T> filter(final Predicate<? super T> predicate) {
        return from(new DelegatingIterables.FilteringIterable<T>(iterable) {
            @Override
            protected boolean accept(T input) {
                return predicate.apply(input);
            }
        });
    }

    @Override
    public LazyFunctionalIterable<T> uniqueElements() {
        List<T> list = asList();
        LinkedHashSet<T> set = new LinkedHashSet<T>(list);
        return from(set);
    }

    @Override
    public LazyFunctionalIterable<T> removeDuplicates() {
        return from(new DelegatingIterables.FilteringIterable<T>(iterable) {
            T previous;
            boolean hasPrevious = false;

            @Override
            protected boolean accept(T input) {
                if (hasPrevious && Objects.eq(input, previous)) {
                    return false;
                } else {
                    hasPrevious = true;
                }
                previous = input;
                return true;
            }
        });

    }

    /**
     * Eagerly fetches all elements.
     * @return an iterable with the elements sorted according to their natural ordering.
     * @throws {@link java.lang.ClassCastException} if T cannot be cast to {@link java.lang.Comparable}
     */
    @Override
    public LazyFunctionalIterable<T> sorted() {
        ArrayList<T> list = new ArrayList<T>(asList());
        @SuppressWarnings("unchecked")
        T[] values = (T[]) list.toArray();
        Arrays.sort(values);
        return from(Arrays.asList(values));
    }

    @Override
    public LazyFunctionalIterable<T> sorted(Comparator<? super T> comparator) {
        List<T> list = new ArrayList<T>(asList());
        Collections.sort(list, comparator);
        return from(list);

    }

    @Override
    public <U extends Comparable<? super U>> LazyFunctionalIterable<T> sortedBy(final Mapper<? super T, U> extractor) {
        // TODO measure performance improvement may be achieved by caching T -> U mappings in a Map<T, U>
        // TODO consider handling null values in iterable (and comparable)
        Comparator<T> comparator = new Comparator<T>() {
            // TODO preallocate map size
            Map<T, U> map = new HashMap<T, U>();

            @Override
            public int compare(T oneT, T anotherT) {
                U oneU = map(oneT);
                U otherU = map(anotherT);
                return oneU.compareTo(otherU);
            }

            U map(T key) {
                U value = map.get(key);
                if (value == null) {
                    value = extractor.apply(key);
                    map.put(key, value);
                }
                return value;
            }
        };
        return sorted(comparator);
    }

    @Override
    public LazyFunctionalIterable<T> forEach(final Block<? super T> operation) {
        for (T element : iterable) {
            operation.apply(element);
        }
        return this;
    }

    @Override
    public <U> LazyFunctionalIterable<U> map(final Mapper<? super T, ? extends U> mapper) {
        return from(new DelegatingIterables.TransformingIterable<T, U>(iterable) {
            @Override
            protected U transform(final T input) {
                return mapper.apply(input);
            }
        });
    }

    @Override
    public <U> LazyFunctionalIterable<U> flatMap(final Mapper<? super T, ? extends Iterable<U>> mapper) {
        return from(new DelegatingIterables.TransformingMultiIterable<T, U>(iterable) {
            @Override
            protected Iterable<U> transform(final T input) {
                return mapper.apply(input);
            }
        });
    }

    /**
     * Returns a map of T -> U mappings, with the same iteration order as that of {@link #iterator()}
     */
    @Override
    public <U> Map<T, U> mapped(Mapper<? super T, ? extends U> mapper) {
        // TODO consider preallocating map size
        Map<T, U> map = new LinkedHashMap<T, U>();
        for (T key : this) {
            map.put(key, mapper.apply(key));
        }
        return Collections.unmodifiableMap(map);
    }

    @Override
    public <U> Map<U,Collection<T>> groupBy(Mapper<? super T,? extends U> mapper) {
        // TODO change signature from Collection<T> to Iterable<T> in line with JDK 8 proposal
        // TODO consider preallocating map size
        Map<U, Collection<T>> map = new LinkedHashMap<U, Collection<T>>();
        for (T value : this) {
            U group = mapper.apply(value);
            Collection<T> collection = map.get(group);
            if (collection == null) {
                collection = new ArrayList<T>();
                map.put(group, collection);
            }
            collection.add(value);
        }
        // TODO return immutable copy
        return Collections.unmodifiableMap(map);
    }

    @Override
    public T reduce(T base, BinaryOperator<T> reducer) {
        T result = base;
        for (T element : iterable) {
            result = reducer.apply(result, element);
        }
        return result;
    }

    @Override
    public <U> U mapReduce(Mapper<? super T, ? extends U> mapper, U base, BinaryOperator<U> reducer) {
        return map(mapper).reduce(base, reducer);
    }
}
